/*
 * (c) ralfoide, http://gamez.googlecode.com/, 2008
 * Project: gamez
 * License TBD
 */

package com.alfray.gamez.gameplay;

import java.io.InputStream;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.IntBuffer;

import javax.microedition.khronos.opengles.GL10;

import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.opengl.GLU;
import android.util.Log;
import android.view.KeyEvent;
import android.view.MotionEvent;
import android.view.View;

import com.alfray.gamez.IUiEventListener;
import com.alfray.gamez.GamezActivity.Context;

//-----------------------------------------------

/**
 * Abstract gameplay base class.
 * 
 * Also acts as an adapter for the {@link IUiEventListener} interface,
 * implementing all methods as empty.
 */
public abstract class Gameplay implements IUiEventListener {

    /** Current horizontal view angle in degrees */
    private float mAngleH = 0; //20.0f;
    /** Current vertical view angle in degrees */
    private float mAngleV = 0; //-20.0f;
    /** Current distance to the camera */
    private float mDistance = 30.0f;
	// GLFixed version of mAngleH. use changeViewRotation to update this. */
	private int xAngleH;
	// GLFixed version of mAngleV. use changeViewRotation to update this. */
	private int xAngleV;
	// GLFixed version of mDistance. use changeViewDistance to update this. */
	private int xDistance;
	
    /** Parameters for light 0.
     */
    private float[] mLight0Params = new float[] {
                    -10, 10, 30, 1,             // position
                    0.2f, 0.2f, 0.2f, 1,    // ambient
                    1, 1, 1, 1,             // diffuse
                    1, 1, 1, 1,             // specular
    };
    
	protected Context mContext;

	protected boolean mRenderSprites = true;
	protected boolean mUseLighting = false;
	protected boolean mUseTexture = false;

	/** Indicates the renderer must setup the scene again. Made public to be read by
	 *  the rendering thread. Use requestSetupScene() to set to true. */
	public boolean mSetupSceneNeeded = true;
	private GameplayThread mGameplayThread;

    public Gameplay(Context context) {
    	mContext = context;
	}

	public void requestSetupScene() {
		mSetupSceneNeeded = true;
	}
	
	public String getMessage() {
		return "";
	}

	public boolean getRenderSprites() {
		return mRenderSprites;
	}

    public void setRenderSprites(final boolean renderSprites) {
		if (mRenderSprites != renderSprites) {
			if (mContext.mRenderThread == null) {
				mRenderSprites = renderSprites;
				mSetupSceneNeeded = true;
			} else {
				mContext.mRenderThread.addPendingTask(new Runnable() {
					public void run() {
						mRenderSprites = renderSprites;
						mSetupSceneNeeded = true;
					}
				});
			}
		}
	}

	public boolean getUseLighting() {
		return mUseLighting;
	}

	public void setUseLighting(final boolean useLighting) {
		if (mUseLighting != useLighting) {
			if (mContext.mRenderThread == null) {
				mUseLighting = useLighting;
				mSetupSceneNeeded = true;
			} else {
				mContext.mRenderThread.addPendingTask(new Runnable() {
					public void run() {
						mUseLighting = useLighting;
						mSetupSceneNeeded = true;
					}
				});
			}
		}
	}

	public boolean getUseTexture() {
		return mUseTexture;
	}

	public void setUseTexture(final boolean useTexture) {
		if (mUseTexture != useTexture) {
			if (mContext.mRenderThread == null) {
				mUseTexture = useTexture;
				mSetupSceneNeeded = true;
			} else {
				mContext.mRenderThread.addPendingTask(new Runnable() {
					public void run() {
						mUseTexture = useTexture;
						mSetupSceneNeeded = true;
					}
				});
			}
		}
	}

	/**
     * Some one-time OpenGL initialization can be made here
     * probably based on features of this particular context
     */
    public void setupScene(GL10 gl, int w, int h) {
    	if (!mSetupSceneNeeded) {
    		return;
    	}
        mSetupSceneNeeded = false;
        
        changeViewRotation(0f, 0f); // side effect to update xHandle{H,V}
        changeViewDistance(0f);  // side effect to update xDistance

        gl.glViewport(0, 0, w, h);

        /*
         * Set our projection matrix. This doesn't have to be done each time we
         * draw, but usually a new projection needs to be set when the viewport
         * is resized.
         */

        float aspect = (float) w / h;
        gl.glMatrixMode(GL10.GL_PROJECTION);
        gl.glLoadIdentity();
        final float near = 1;
        final float far = mDistance + 50;

        
        float fovy = 2.0f * (float) Math.atan2(8.0f / 2, 20);
        fovy = 180.0f * fovy / (float) Math.PI;
        GLU.gluPerspective(gl, fovy, aspect, near, far);

        gl.glDisable(GL10.GL_DITHER);

        gl.glEnableClientState(GL10.GL_VERTEX_ARRAY);
        gl.glEnable(GL10.GL_CULL_FACE);
        gl.glEnable(GL10.GL_DEPTH_TEST);
        
    	// gl.glPixelStorei(GL10.GL_UNPACK_ALIGNMENT, 1); ==> BUG: GL_INVALID_ENUM
        if (mUseTexture) {
	        gl.glHint(GL10.GL_PERSPECTIVE_CORRECTION_HINT, GL10.GL_FASTEST); // NICEST);
			gl.glEnableClientState(GL10.GL_TEXTURE_COORD_ARRAY);
			
			gl.glShadeModel(GL10.GL_FLAT);
        } else {
            gl.glHint(GL10.GL_PERSPECTIVE_CORRECTION_HINT, GL10.GL_FASTEST);
			gl.glDisableClientState(GL10.GL_TEXTURE_COORD_ARRAY);
            gl.glActiveTexture(GL10.GL_TEXTURE0); // TMU0
			gl.glDisable(GL10.GL_TEXTURE_2D);
	        gl.glActiveTexture(GL10.GL_TEXTURE1); // TMU1
			gl.glDisable(GL10.GL_TEXTURE_2D);
        }

        if (mUseLighting) {
            gl.glShadeModel(GL10.GL_SMOOTH);
	        gl.glEnable(GL10.GL_LIGHTING);
	        gl.glEnable(GL10.GL_LIGHT0);
            gl.glEnableClientState(GL10.GL_NORMAL_ARRAY);
	
	        // Test: remove global ambient color
	        gl.glLightModelfv(GL10.GL_LIGHT_MODEL_AMBIENT,
	                        new float[] { 0, 0, 0, 1 }, 0);
	
	        setupLights(gl);
        } else {
            gl.glShadeModel(GL10.GL_FLAT);
	        gl.glDisable(GL10.GL_LIGHTING);
	        gl.glDisable(GL10.GL_LIGHT0);
            gl.glDisableClientState(GL10.GL_NORMAL_ARRAY);
            // gl.glEnableClientState(GL10.GL_COLOR_ARRAY);
        }
    }

    /**
     * Initializes the lights in the given GL context.
     */
    protected void setupLights(GL10 gl) {
        gl.glLightfv(GL10.GL_LIGHT0, GL10.GL_POSITION,  // xyzw pos
                        mLight0Params, 0);
        gl.glLightfv(GL10.GL_LIGHT0, GL10.GL_AMBIENT,   // rgba color & intensity
                        mLight0Params, 4);
        gl.glLightfv(GL10.GL_LIGHT0, GL10.GL_DIFFUSE,   // rgba color & intensity
                        mLight0Params, 8);
        gl.glLightfv(GL10.GL_LIGHT0, GL10.GL_SPECULAR,   // rgba color & intensity
                        mLight0Params, 12);
    }

    /**
     * Associates a given texture index to a given TMU renderer.
     *
     * @param gl The GL context
     * @param tmu_index TMU0 or TMU1
     * @param texture_index The GL texture index (1..n)
     */
	protected void associateTexture(GL10 gl, int tmu_index, int texture_index) {
		setupTexture(gl, tmu_index, texture_index, GL10.GL_REPLACE);
	}

    /**
     * Associates a given texture index to a given TMU renderer.
     *
     * @param gl The GL context
     * @param tmu_index TMU0 or TMU1
     * @param texture_index The GL texture index (1..n)
     * @param mode GL_REPLACE, GL_BLEND, etc.
     */
	protected void setupTexture(GL10 gl, int tmu_index, int texture_index, int mode) {
	    if (mUseTexture) {
	        gl.glActiveTexture(tmu_index); // TMU0 or TMU1
	    	gl.glBindTexture(GL10.GL_TEXTURE_2D, texture_index);
			gl.glEnable(GL10.GL_TEXTURE_2D);
	        gl.glTexEnvx(GL10.GL_TEXTURE_ENV, GL10.GL_TEXTURE_ENV_MODE, mode);
	    }
	}

    /**
     * Loads a texture from a bitmap in resources.
     *
     * @param gl The GL context
     * @param res_id The id of the image to load
     * @param texture_format The desired GL target format, one of GL_RGB.
     * @param tex_num The GL texture index (1..n)
     */
    protected void loadTexture(GL10 gl, int res_id, int texture_format, int tex_num) {
    	InputStream is = mContext.mAndroidContext.getResources().openRawResource(res_id);
    	Bitmap bmp = BitmapFactory.decodeStream(is);
    	int w = bmp.getWidth();
    	int h = bmp.getHeight();

    	assert w > 0;
    	assert h > 0;
    	
    	int[] argb = new int[w*h];
    	bmp.getPixels(argb, // pixels
    			0, // offset
    			w, // stride
    			0, // x
    			0, // y
    			w, // width
    			h  // height
    			);

    	ByteBuffer byte_buf;

    	switch(texture_format) {
		case GL10.GL_RGB:
	    	byte_buf = ByteBuffer.allocateDirect(w*h*3);
	    	byte_buf.order(ByteOrder.nativeOrder());
	    	
	    	for(int i : argb) {
	    		byte_buf.put((byte)(i >> 16));
	    		byte_buf.put((byte)(i >>  8));
	    		byte_buf.put((byte)(i >>  0));
	    	}

    		break;
		case GL10.GL_RGBA:
	    	byte_buf = ByteBuffer.allocateDirect(w*h*4);
	    	byte_buf.order(ByteOrder.nativeOrder());
	    	IntBuffer int_buf = byte_buf.asIntBuffer();
	    	for(int i : argb) {
	    		// argb to abgr
	    		i = (i & 0xFF00FF00) | ((i >> 16) & 0x000000FF) | ((i << 16) & 0x00FF0000);
	    		int_buf.put(i);
	    	}
	    	break;
		default:
			throw new IllegalArgumentException("Unsupported texture_format");
    	}
    	
    	byte_buf.position(0);
    	
        gl.glBindTexture(GL10.GL_TEXTURE_2D, tex_num);

        gl.glTexParameterx(GL10.GL_TEXTURE_2D, GL10.GL_TEXTURE_WRAP_S, GL10.GL_CLAMP_TO_EDGE);
        gl.glTexParameterx(GL10.GL_TEXTURE_2D, GL10.GL_TEXTURE_WRAP_T, GL10.GL_CLAMP_TO_EDGE);
        gl.glTexParameterx(GL10.GL_TEXTURE_2D, GL10.GL_TEXTURE_MAG_FILTER, GL10.GL_LINEAR);//  NEAREST);
        gl.glTexParameterx(GL10.GL_TEXTURE_2D, GL10.GL_TEXTURE_MIN_FILTER, GL10.GL_LINEAR);//NEAREST);

    	gl.glTexImage2D(
    			GL10.GL_TEXTURE_2D, // target 
    			0, // level
    			texture_format, // internalformat
    			w, // width
    			h, // height
    			0, // border
    			texture_format, // format
    			GL10.GL_UNSIGNED_BYTE, // type
    			byte_buf // pixels
    			);
    }

    /*
     * Draws one frame on the GL animation.
     * 
     * Called for every frame.
     */
    public void drawScene(GL10 gl, int w, int h) {
        /*
         * Usually, the first thing one might want to do is to clear
         * the screen. The most efficient way of doing this is to use
         * glClear(). However we must make sure to set the scissor
         * correctly first. The scissor is always specified in window
         * coordinates:
         */
        gl.glClearColor(0,0,0,1); // clear to black
        gl.glClear(GL10.GL_COLOR_BUFFER_BIT | GL10.GL_DEPTH_BUFFER_BIT);

        /*
         * Now we're ready to draw some 3D object
         */
        gl.glMatrixMode(GL10.GL_MODELVIEW);
        gl.glLoadIdentity();
        gl.glTranslatex(0, 1 << 16, 0 - xDistance);
        gl.glRotatex(xAngleH, 0, 1 << 16, 0);
        gl.glRotatex(xAngleV, 1 << 16, 0, 0);
    }

    /**
     * User requested to rotate the current 3d view.
     *
     * @param h The delta in the horizontal direction. -1=left, 1=right.
     * @param v The delta in the vertical direction. -1=down, 1=up.
     */
    public void changeViewRotation(float h, float v) {
        mAngleH -= h;
        mAngleV += v;
        xAngleH = (int)(mAngleH * 65536);
        xAngleV = (int)(mAngleV * 65536);
    }
 
    /**
     * Adjusts the distance to the camera by the given delta.
     * Positive values make object look farther.
     *
     * @param delta The distance to add or substract. +1=farther, -1=closer
     */
    public void changeViewDistance(float delta) {
        mDistance += delta;
        xDistance = (int)(mDistance * 65536);
    }

	//-----------------------------------------------

	public void startThread() {
		if (mGameplayThread == null) {
			mGameplayThread = new GameplayThread();
			mGameplayThread.start();
		}
	}

	public void stopThread() {
		if (mGameplayThread != null) {
			mGameplayThread.waitForStop();
			mGameplayThread = null;
		}
	}

	public void pauseThread(boolean pause) {
		if (mGameplayThread != null) {
			mGameplayThread.pause(pause);
		}
	}
	
	/** Gameplay thread update.
	 * The thread is started only after the setup of the scene.
	 * 
	 * @param context The espresso context
	 * @return How many milliseconds to sleep till next call
	 */
	public abstract int threadUpdate(Context context);
	
	class GameplayThread extends Thread {

	    private static final String TAG = "GameplayThread";

		private static final int PAUSE_TIME_MS = 50; // in milliseconds

		private boolean mContinue = true;
		private boolean mIsPaused = false;

		public GameplayThread() {
			super(TAG);
	        this.setPriority(Thread.currentThread().getPriority());
		}
		
		public void pause(boolean pause) {
			mIsPaused = pause;
		}

		@Override
		public void run() {
			super.run();
			while (mContinue) {
            	if (mIsPaused) {
            		try {
            			Thread.sleep(PAUSE_TIME_MS);
            		} catch (InterruptedException e) {
            			Log.e(TAG, "Thread.sleep interrupted", e);
            		}
            		continue;
            	}

            	int sleep_ms = threadUpdate(mContext);
		    	try {
		    		if (sleep_ms > 0) {
		    			Thread.sleep(sleep_ms); // milliseconds
		    		}
				} catch (InterruptedException e) {
					Log.e(TAG, "threadUpdate.sleep interrupted", e);
				}
			}
		}

		public void waitForStop() {
			mContinue = false;
			try {
				this.join();
			} catch (InterruptedException e) {
				Log.e(TAG, "waitForStop.join interrupted", e);
			}
		}
	}

	//-----------------------------------------------

	/**
	 * Base implementation the method to handle touch motion events that
	 * does nothing.
	 * <p/>
	 * @param event The motion event.
	 * @return True if the event was handled by the method.
	 */
	public boolean onTouchEvent(MotionEvent event) {
		return false;
	}

    public boolean onTrackballEvent(MotionEvent event) {
      return false;
  }

    /**
	 * Base implementation the method to handle clicks that
	 * does nothing.
	 */
	public void onClick(View view) {
	}
	
	/**
	 * Called when a key down event has occurred.
	 * <p/>
	 * This base implementation does nothing and returns false.
	 * 
	 * @param keyCode The value in event.getKeyCode().
	 * @param event Description of the key event.
	 * @return If you handled the event, return true.
	 *         If you want to allow the event to be handled by the next receiver, return false.
	 */
	public boolean onKeyDown(int keyCode, KeyEvent event) {
		return false;
	}
	
	/**
	 * Called when multiple down/up pairs of the same key have occurred in a row.
	 * <p/>
	 * This base implementation does nothing and returns false.
	 * 
	 * @param keyCode The value in event.getKeyCode().
	 * @param count Number of pairs as returned by event.getRepeatCount().
	 * @param event Description of the key event.
	 * @return If you handled the event, return true.
	 *         If you want to allow the event to be handled by the next receiver, return false.
	 */
	public boolean onKeyMultiple(int keyCode, int count, KeyEvent event) {
		return false;
	}
	
	/**
	 * Called when a key up event has occurred.
	 * <p/>
	 * This base implementation does nothing and returns false.
	 * 
	 * @param keyCode The value in event.getKeyCode().
	 * @param event Description of the key event.
	 * @return If you handled the event, return true.
	 *         If you want to allow the event to be handled by the next receiver, return false.
	 */
	public boolean onKeyUp(int keyCode, KeyEvent event) {
		return false;
	}
}


